Canigó - Servei de WebServices 2.3.x
>
SERVEI DE WEBSERVICES
IntroduccióPropòsitAquest servei permet configurar i usar de forma senzilla la infraestructura de Web Services en dues modalitats:
L'enfocament d'aquest servei és el de simplificar tant la definició de Web Services a partir de serveis Java simples (que no tindran dependències amb la implementació particular de Web Services) així com la de facilitar la invocació a Web Services externs. Context i Escenaris d'ÚsEl Servei d'Integració de WebServices es troba ubicat dins els serveis continguts a la capa de Dades/Integració de Canigó.
Versions i DependènciesLes dependències descrites a la següent url son requerides per tal de compilar i fer funcionar el projecte: A qui va dirigitAquest document va dirigit als següents perfils:
Documents i Fonts de Referència
Descripció DetalladaArquitectura i ComponentsEls components podem classificar-los en:
JavaDoc: http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-webservices/apidocs/index.html Instal.lació i ConfiguracióInstal.lacióLa instal.lació del servei requereix de la utilització de la llibreria 'canigo-services-webservices' i les dependències indicades a l'apartat 'Introducció-Versions i Dependències'. La instal- lació passa per modificar el pom.xml de l'aplicació per incloure les llibreries del Connector com a dependència. <canigo.version>2.3.20</canigo.version> <dependency> <groupId>canigo</groupId> <artifactId>canigo-services-webservices</artifactId> <version>${canigo.version}</version> </dependency>
ConfiguracióLa configuració del Servei d'Integració de Web Services implica els següents pasos:
Definició del Servei
En aquest pas indicarem el bean del servei de Web Services de Canigó i la implementació que es farà servir. En l'actualitat s'ofereix la implementació Podem definir les següents propietats:
Exemple: <bean name="webServicesService" class="net.gencat.ctti.canigo.services.webservices.impl.WebServicesServiceImpl" ... </bean> És a dir, si ens volem comunicar amb un Webservice ja existent farem ús de la propietat 'exportedInterfaces', mentre que si volem publicar un WebService usarem la propietat 'importedInterfaces'. Definició de les Interfícies Pare d'Exportació i Importació de Serveis
Usar el següent codi:
<bean abstract="true" id="exportedInterfaceDefinition" class="net.gencat.ctti.canigo.services.webservices.impl.ExportedInterfaceImpl"/> <bean abstract="true" id="importedInterfaceDefinition" class="net.gencat.ctti.canigo.services.webservices.impl.ImportedInterfaceImpl"/> Amb aquesta secció de declaracions es pretén establir les classes que permeten fer la definició dels WebServices, i estalviar així haver de repetir la declaració de la classe sencera (els noms de package són llargs) per cada bean d'importació/exportació que declarem. L'atribut "abstract=true" implica que aquests beans no s'instancien, només serveixen per fer-ne redefinicions. NOTA: Només definirem un i/o l'altre segons ens comuniquem amb un webservice i/o publiquem un webservice Definició dels Serveis a Exportar ![]() Fitxer de configuració: canigo-services-webservices.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring Definir els serveis a exportar dins la propietat 'exportedInterfaces' del bean definició del servei. Aquesta propietat és una llista dels beans que exportarem. Per cada bean a exportar farem que el seu parent sigui la definició abstracta ('exportedInterfaceDefinition') i es definiran les següents propietats:
Exemple:
<bean name="webServicesService" parent="WebServiceDefinition"> <property name="exportedInterfaces"> <list> <bean parent="exportedInterfaceDefinition"> <property name="name" value="testService"/> <property name="implementation" value="net.gencat.ctti.samples.webservices.TestServiceImpl"/> <property name="localInterface" value="net.gencat.ctti.samples.webservices.TestService"/> </bean> </list> </property> ... </bean> Només especificant les tres propietats s'aconsegueix que un servei Java sigui accessible per Web Services. En aquest exemple, es podria obtenir el WDSL (Web Services Description Language) generat a una URL semblant a: http://localhost:8080/canigo-samples-webservices/testService?wsdl On testService es correspon al nom definit en el xml del servlet._ Per últim, s'ha de crear un nou mapping per què el servlet principal gestioni les peticions als WebServices (la URL /ws/* és arbitraria, pot ser qualsevol altre que no tingui conflictes amb altres serveis). <servlet-mapping> <servlet-name>nom_servlet</servlet-name> <url-pattern>/ws/*</url-pattern> </servlet-mapping> Definició dels Serveis a Importar ![]() Fitxer de configuració: canigo-services-webservices.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring Definir els serveis a importar dins la propietat 'importedInterfaces' del bean definició del servei. Per cada bean a importar farem que el seu parent sigui la definició abstracta ('importedInterfaceDefinition'). Existeixen 2 possibilitats d'integració amb serveis externs:
En aquest cas, definirem les següents propietats:
Exemple:
<bean parent="importedInterfaceDefinition"> <property name="name" value="GoogleSearch" /> <property name="serviceURL" value="http://localhost:8080/canigo-samples-webservices/testService"/> <property name="localInterface" value="net.gencat.ctti.samples.webservices.TestService"/> </bean>
Utilització del ServeiCanigó exposa una interfície d'utilització, "net.gencat.ctti.canigo.webservices.WebServicesService", que amaga la implementació real utilitzada. D'aquesta forma diverses implementacions són fàcilment configurables en funció de les necessitats de l'aplicació. Aquesta interfície permet obtenir un webservice i treballar-hi posteriorment com si d'una classe normal es tractés. La interfície té el següent aspecte:
package net.gencat.ctti.canigo.services.webservices; /** * Interface which allows the user to retrieve a Web Service interface from a given configuration */ public interface WebServicesService { ... public Object getWebService(String serviceName); } Aquesta interfície exposa un únic mètode 'getWebService' que permet obtenir una instància del Web Service definit amb el nom del paràmetre passat. Aquest nom correspon al definit a la propietat 'name' de la definició dels serveis importats o exportats (veure apartat 'Configuració'). És responsabilitat de programador identificar correctament la interfície de servei tant a la configuració com a l'hora de fer ús del servei, fent el corresponent "cast". Una vegada realitzat el cast, podem treballar-hi com si d'un servei local es tractés. Addicionalment, s'ofereix una classe 'net.gencat.ctti.canigo.services.webservices.WebServicesServiceUtils' que proporciona el mètode 'getWebService(HttpServletRequest request, String serviceName)'. Aquest mètode permet que des de les classes de presentació s'obtingui de forma directa una referència a una instància de webservice. Eines de SuportGeneració de Classes a partir de WDSLPer a generar les classes necessàries per importar un servei extern a partir d'un WDSL (veure apartat 'Configuració-Definició dels Serveis a Importar'), ho farem amb l'eina ANT, en un fitxer build.xml amb el següent contingut:
<project name="Generate client (Axis)" default="generate-client"> <property name="output.dir" value="src"/> <property name="wsdl.file" value="resources/HolaMon.wsdl"/> <property name="lib" value="lib"/> <path id="axis.classpath"> <fileset dir="${lib}"> <include name="**/*.jar"/> </fileset> </path> <taskdef resource="axis-tasks.properties" classpathref="axis.classpath" /> <target name="generate-client"> <axis-wsdl2java output="${output.dir}" testcase="true" verbose="true" url="${wsdl.file}" > </axis-wsdl2java> </target> </project> Les llibreries necessàries per a la correcta execució són les pròpies d'ANT juntament amb l'eina Axis-Ant:
Integració amb Altres ServeisDefinició de les Excepcions InternacionalitzadesEl servei defineix vàries excepcions amb diferents claus de missatges. Aquestes són:
canigo.services.WebServices.lookup_failed=lookup failed for {0} canigo.services.WebServices.bad_bean_configuration=bad configuration for {0} canigo.services.WebServices.remote_method_invocation_failed=remote method {0} failed for bean {1} Per tant, han d'existir en el fitxer de recursos. Preguntes Freqüentsjava.lang.NoSuchMethodError
Quan arranquem l'aplicació ens apareix al log el missatge: java.lang.NoSuchMethodError: javax.xml.namespace.QName.<init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String) [10/03/06 17:18:32:130 CET] 2f75d693 ContextLoader E org.springframework.web.context.ContextLoader TRAS0014I: The following exception was logged java.lang.NoSuchMethodError:javax.xml.namespace. QName: method <init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V not found at at org.codehaus.xfire.aegis.type.DefaultTypeMappingRegistry.<clinit> (DefaultTypeMappingRegistry.java:54).null(Unknown Source) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java(Compiled Code)) at org.springframework.util.ClassUtils.forName(ClassUtils.java:88) at org.springframework.beans.factory.support.BeanDefinitionReaderUtils. createBeanDefinition(BeanDefinitionReaderUtils.java:65) at org.springframework.beans.factory.xml.DefaultXmlBeanDefinitionParser. parseBeanDefinitionElement(DefaultXmlBeanDefinitionParser.java:369) ... Si es fa exportació de WebServices mitjançant XFire, hem de tenir cura de quina és la versió del Servidor d'Aplicacions que es fa servir. Els Servidors d'Aplicacions tenen per defecte algunes llibreries ja incorporades que poden afectar a les aplicacions que vulguin utilitzar noves versions d'aquestes llibreries. La política per defecte dels Servidors d'Aplicacions sol ser que en cas de que l'aplicació importi una classe es carregui primer la disponible al Servidor d'Aplicacions. D'aquí que en el cas mostrat, la classe 'QName' és trobada al Servidor d'Aplicacions i no es té en compte la que es troba a la llibreria més nova de l'aplicació. Per a canviar aquest comportament, existeix la possibilitat de canviar la precedència de la càrrega de classes per aplicació, de forma que si una classe està en una llibreria del servidor i en una llibreria del módul de l'aplicació, sigui prioritària aquesta última enlloc de la primera. Comprovacions prèvies Abans de fer cap canvi de configuració ens hem d'assegurar que es troba en el fitxer de dependències la llibreria 'stax-api-1.0.jar' i que s'afegirà al war creat. <dependency>
<groupId>stax</groupId>
<artifactId>stax-api</artifactId>
<version>1.0</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
Aquesta llibreria conté la versió de QName que necessita XFire. Adicionalment comprovar que hem definit les següents dependències:
Per totes elles assegurar-se de que s'incorporaran al war amb el tag '<war.bundle>true</war.bundle>'. Websphere A Websphere, una vegada fet el desplegament de l'aplicació tornar a seleccionar 'Applications>Enterprise Applications' i seleccionar l'aplicació. Des de la pestanya 'Configuration' realitzar els següents canvis:
Aplicar els canvis. En aquest moment iniciar l'aplicació i comprovar que no es dóna el missatge previ de conflicte.
WebLogic
En WebLogic podem fer el canvi directament al fitxer 'weblogic.xml' introduint el valor 'true' en el tag 'prefer-web-inf-classes': <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE weblogic-web-app PUBLIC "-//BEA Systems, Inc.//DTD Web Application 8.1//EN" "http://www.bea.com/servers/wls810/dtd/weblogic810-web-jar.dtd"> <weblogic-web-app> <container-descriptor> <prefer-web-inf-classes>true</prefer-web-inf-classes> </container-descriptor> </weblogic-web-app> javax.xml.stream.FactoryConfigurationErrorApareixl'Error 'javax.xml.stream.FactoryConfigurationError: Provider null could not be instantiated: java.lang.NullPointerException'
Si aquest error es dona en una aplicació desplegada al Servidor d'Aplicacions, tenim 2 possibles solucions:
a) Crear un fitxer per definir la factoria 'XMLInputFactory' (opció més recomanada) Crear un directori 'META-INF/services' (dins webapp si és una aplicació Web) i en aquest directori crear: - Fitxer 'javax.xml.stream.XMLInputFactory' (sense extensió) El contingut d'aquest fitxer ha de ser 'com.ctc.wstx.stax.WstxInputFactory'. Aquest comportament només funcionarà si no existeix el fitxer 'jaxp.properties' en el subdirectori 'jre\lib' de Java (veure següent alternativa). Així doncs, cal assegurar-se de que aquest fitxer (explicat en l'altra alternativa) no existeixi.
b) Editar un fitxer 'jaxp.properties' a la localització del subdirectori 'jre\lib' amb la següent informació:
javax.xml.transform.TransformerFactory=org.apache.xalan.processor.TransformerFactoryImpl javax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl javax.xml.parsers.DocumentBuilderFactory=org.apache.xerces.jaxp.DocumentBuilderFactoryImpl javax.xml.stream.XMLInputFactory=com.ctc.wstx.stax.WstxInputFactory El subdirectori 'jre\lib' l'haurem de cercar en el directori del JRE que s'estigui fent servir. Així, per exemple en Websphere accedirem al directori 'AppServer\jre\lib' del directori d'instal.lació. ExemplesExemple de TestClasse: 'net.gencat.ctti.canigo.services.webservices.test.WebServicesServiceTest' Per tractar-se d'un Test Unitari l'obtenció del servei es realitza de forma directa amb 'ClassPathXMLApplicationContext'. Cal recordar que podrem definir en els nostres beans de l'aplicació el servei sense necessitar d'accedir-hi amb 'ClassPathXmlApplicationContext'. En aquest cas és necessari per tractar-se d'un test unitari. Exemple d'accés directe: Obtenim el context Spring de test, aixo no cal fer-ho explícitament BeanFactory beanFactory = new ClassPathXmlApplicationContext("webServicesContext.xml"); Obtenim el servei pròpiament dit WebServicesService service = (WebServicesService) beanFactory. getBean(WebServicesService.WEB_SERVICES_BEAN_FACTORY_KEY); Ara es pot fer servir l'interface WebServicesService per accedir a un Web Serice remot ... GoogleSearchPort google = (GoogleSearchPort) service.getWebService("GoogleSearch");
google.doGoogleSearch(...);
Exemple d'accés des d'un Action: GoogleSearchPort google = WebServicesServiceUtils.getWebService(request,"GoogleSearch");
google.doGoogleSearch(...);
Exemple de Publicació d'un Servei de Codis PostalsEn aquest exemple veurem com podem publicar un servei de forma senzilla mitjançant Canigó i XFire. Suposem que volem publicar un servei que permet obtenir les localitats associades a un codi postal. Com a exemple pràctic i bastant real farem servir un servei extern ja existent de GeoNames.
Creació de la InterfícieEn primer lloc creem una interfície Java en la que definim quins mètodes oferirem.
package net.gencat.ctti.canigo.samples.geo; import java.util.Collection; public interface GeoNamesService { public Collection getLocationsPostalCode(String aPostalCode); } En aquest cas trobem un mètode que a partir d'un codi postal ens retornarà una col.lecció de poblacions coincidents amb el codi postal. Implementació de la InterfícieA continuació definim la implementació de la interfície:
public class GeoNamesServiceImpl implements GeoNamesService { public GeoNamesServiceImpl() { super(); // TODO Auto-generated constructor stub } public Collection getLocationsPostalCode(String aPostalCode) { URL url; ArrayList list = new ArrayList(); try { url = new URL("http://ws.geonames.org/postalCodeSearch?postalcode=" + aPostalCode + "&country=ES&maxRows=10"); try { SAXReader reader = new SAXReader(); Document document = reader.read(url); // XES: Access with XPath List listNodes = document.selectNodes( "//geonames/code/name" ); for ( Iterator i = listNodes.iterator(); i.hasNext(); ) { DefaultElement element = (DefaultElement)i.next(); String text = element.getText(); list.add(text); System.out.println(text); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } catch (MalformedURLException e2) { // TODO Auto-generated catch block e2.printStackTrace(); } return list; } En l'exemple mostrat s'obté la informació a partir d'un servei proporcionat per GeoNames i es parseja el resultat rebut per crear una col.lecció. No és propòsit d'aquest exemple mostrar com obtindrem en les nostres aplicacions la geocodificació dels codis postals, però hem cregut convenient oferir un exemple molt real. Per tant hauria estat suficient amb deixar al lector una implementació de la interfície que fes un càlcul simple. Una vegada tenim implementada la classe podem publicar-la fàcilment com un WebService: Publicació del Servei
<bean abstract="true" id="exportedInterfaceDefinition" class="net.gencat.ctti.canigo.services.webservices.impl.ExportedInterfaceImpl"/> <bean name="webServicesService" class="net.gencat.ctti.canigo.services.webservices.impl.WebServicesServiceImpl"> <property name="exportedInterfaces"> <list> <bean parent="exportedInterfaceDefinition"> <property name="name" value="geoService"/> <property name="implementation" value="net.gencat.ctti.canigo.samples.geo.GeoNamesServiceImpl"/> <property name="localInterface" value="net.gencat.ctti.canigo.samples.geo.GeoNamesService"/> </bean> </list> </property> </bean> La publicació del servei és tan senzilla com el codi a dalt mostrat, on dins la propietat 'exportedInterfaces' del bean 'webServicesService' s'ha definit una exportació amb la següent informació:
A continuació, podem crear un war i desplegar-lo en un servidor d'aplicacions. http://localhost:9080/<nom context app>/geoService?wsdl
El nom 'geoService' correspon al valor de l'atribut 'name' que hem especificat prèviament al fitxer de configuració.
Accés des d'un ClientSi volem accedir al servei publicat podem fer ús de la importació seguint els següents pasos:
1) Definició en el fitxer de configuració de la importació
<bean abstract="true" id="importedInterfaceDefinition" class="net.gencat.ctti.canigo.services.webservices.impl.ImportedInterfaceImpl"/> <bean name="webServicesService" class="net.gencat.ctti.canigo.services.webservices.impl.WebServicesServiceImpl"> <property name="importedInterfaces"> <list> <bean parent="importedInterfaceDefinition"> <property name="name" value="geoNamesService" /> <property name="serviceURL" value="http://localhost:9080/canigo-samples-geo/geoService"/> <property name="localInterface" value="net.gencat.ctti.canigo.samples.geo.GeoNamesService"/> </bean> </list> </property> On dins la propietat 'importedInterfaces' s'ha definit una importació amb la següent informació:
2) Accés des d'una classe Una vegada definit això l'accés seria tan simple com l'exemple mostrat a continuació:
GeoNamesService geoService = (GeoNamesService)webServicesService.getWebService("geoNamesService"); Collection locations = geoService.getLocationsPostalCode("08031"); S'ha obtingut des del bean 'webServicesService' la interfície 'GeoNamesService' a partir del nom que havíem definit a la propietat 'name' en el pas 1). A partir d'aquest moment es pot fer ús de la interfície com si d'una classe local es tractés. Encara millor, podem definir el servei sense que es sabés que fem ús del web service definint una injecció de tipus factoria: <bean id="geoNamesService" factory-bean="webServicesService" factory-method="getWebService"> <constructor-arg><value>geoNamesService</value></constructor-arg> </bean> En aquest cas estem especificant que el bean amb id 'geoNamesService' s'obtindrà a partir de la crida "getWebService('geoNamesService')". Exemple d'ús de Ajax amb el Servei PublicatNOTA: Tot i no ser objectiu del present document es mostra en aquest apartat com podem utilitzar el servei 'geoNamesService' (que permet comunicar-se amb el WebService) per presentar les poblacions a l'usuari segons el codi postal introduit usant Ajax:
En primer lloc, publicarem el servei definit en el pas anterior (<bean id="geoNamesService" factory-bean="webServicesService" ...) introduint al fitxer 'src/main/resources/dwr/dwr.xml' que volem des del client Web accedir a aquest servei:
<create creator="spring" javascript="geoNamesService"> <param name="beanName" value="geoNamesService"/> </create> En el camp 'javascript' s'indica com a valor el nom del fitxer javascript que es generarà de forma automàtica i que serà usat des del client. Així, en la pàgina JSP hem d'afegir una referència tal i com es mostra a continuació: <script src="<c:url value="/AppJava/dwr/interface/geoNamesService.js"/>"> </script> Una vegada definida aquesta referència, usarem la llibreria 'prototype' per definir una classe que controlarà l'event de canvi de valor en el camp d'introducció del codi postal: <script> var locations; function closeSuggestBox() { $('suggestBoxElement').innerHTML = ''; $('suggestBoxElement').style.visibility = 'hidden'; } // remove highlight on mouse out event function suggestBoxMouseOut(obj) { document.getElementById('pcId'+ obj).className = 'suggestions'; } // the user has selected a place name from the suggest box function suggestBoxMouseDown(obj) { closeSuggestBox(); var placeInput = $('city'); placeInput.value = locations[obj]; } var PostalCodeWatcher = Class.create(); PostalCodeWatcher.prototype = { initialize: function(field) { this.field = $(field); this.field.onchange = this.getLocationsPostalCode.bindAsEventListener(this); }, getLocationsPostalCode: function(evt){ if ($F(this.field)) { geoNamesService.getLocationsPostalCode( $F(this.field),{ callback:function(dataFromServer) {updateLocations(dataFromServer); }, timeout:5000 } ); } } }; var watcher = new PostalCodeWatcher('zip'); // function to highlight places on mouse over event function suggestBoxMouseOver(obj) { document.getElementById('pcId'+ obj).className = 'suggestionMouseOver'; } function updateLocations(dataFromServer) { $('suggestBoxElement').style.visibility = 'visible'; $('suggestBoxElement').innerHTML = '<small><i>loading ...</i></small>'; locations = dataFromServer; //alert(DWRUtil.toDescriptiveString(dataFromServer,1)); if (locations.length > 1) { $('suggestBoxElement').style.visibility = 'visible'; var suggestBoxHTML = ''; // iterate over places and build suggest box content for (i=0;i< locations.length;i++) { suggestBoxHTML += "<div class='suggestions' id=pcId" + i + " onmousedown='suggestBoxMouseDown(" + i + ")' onmouseover='suggestBoxMouseOver(" + i +")' onmouseout='suggestBoxMouseOut(" + i +")'> " + locations[i] +'</div>'; } $('suggestBoxElement').innerHTML = suggestBoxHTML; } else { if (dataFromServer.length == 1) { $("city").value=locations[0]; } closeSuggestBox(); } } </script> És important anotar (consultar la llibreria prototype per a més referència) els següents punts:
NOTA: La funció $F() de prototype permet obtenir el valor de qualsevol tipus de input (enlloc d'haver d'usar diferents funcions segons el tipus de component).
Degut a que la funció ens retorna una col.lecció de poblacions es tracten els casos d'un valor retornat -es copiarà directament al component destí- o de més d'un valor -es mostraran vàries capes per simular una selecció a l'usuari. Per últim, podem definir l'estil d'aquestes seleccions:
<style> #suggestBoxElement {border: 1px solid #8FABFF; visibility:hidden; text-align: left; white-space: nowrap; background-color: #eeeeee;} .suggestions { font-size: 14;background-color: #eeeeee; } .suggestionMouseOver { font-size: 14;background: #3333ff; color: white; } </style> Exemple de resultat mostrat en introduir un codi postal de múltiples poblacions: ![]() En seleccionar un valor aquest és copiat al component. ![]() |